iT邦幫忙

2025 iThome 鐵人賽

DAY 5
1

昨天我們加上了中介層:logging、recovery、config。
今天要邁出關鍵的一步:建立第一個業務相關 API —— /search

雖然我們還沒有連 Elasticsearch,但這一步的目標是:

  • 定義 API 介面(URL、輸入參數、輸出格式)
  • 先回傳固定假資料,作為之後串接真資料的基礎

Step 1:設計 API 規格

我們希望 /search API 可以做到:

  • 使用 GET /search?q=keyword 查詢
  • 回傳 JSON,格式類似:
{
  "query": "golang",
  "hits": [
    { "id": 1, "title": "Learning Go" },
    { "id": 2, "title": "Go Concurrency Patterns" }
  ]
}


Step 2:新增 Handler

main.go 增加一個 handler:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

// 假資料
type SearchResult struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
}

type SearchResponse struct {
	Query string         `json:"query"`
	Hits  []SearchResult `json:"hits"`
}

func searchHandler(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query().Get("q")
	if query == "" {
		http.Error(w, "missing query parameter: q", http.StatusBadRequest)
		return
	}

	resp := SearchResponse{
		Query: query,
		Hits: []SearchResult{
			{ID: 1, Title: "Learning Go"},
			{ID: 2, Title: "Go Concurrency Patterns"},
		},
	}

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(resp); err != nil {
		http.Error(w, fmt.Sprintf("encode error: %v", err), http.StatusInternalServerError)
	}
}

func main() {
	cfg := LoadConfig()

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", healthHandler)
	mux.HandleFunc("/search", searchHandler)

	handler := LoggingMiddleware(RecoveryMiddleware(mux))

	log.Printf("Server listening on %s", cfg.Port)
	if err := http.ListenAndServe(cfg.Port, handler); err != nil {
		log.Fatal(err)
	}
}


Step 3:測試 /search

新增 search_test.go

package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestSearchHandler(t *testing.T) {
	tests := []struct {
		name       string
		query      string
		wantStatus int
		wantHits   int
	}{
		{
			name:       "valid query",
			query:      "golang",
			wantStatus: http.StatusOK,
			wantHits:   2,
		},
		{
			name:       "missing query",
			query:      "",
			wantStatus: http.StatusBadRequest,
			wantHits:   0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/search?q="+tt.query, nil)
			w := httptest.NewRecorder()

			searchHandler(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("status got %d, want %d", w.Code, tt.wantStatus)
			}

			if tt.wantStatus == http.StatusOK {
				var resp SearchResponse
				if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
					t.Fatalf("failed to parse JSON: %v", err)
				}
				if len(resp.Hits) != tt.wantHits {
					t.Errorf("hits got %d, want %d", len(resp.Hits), tt.wantHits)
				}
			}
		})
	}
}


Step 4:實際執行

啟動伺服器:

go run .

測試:

curl "http://localhost:8080/search?q=go2"

輸出應該是:

{
  "query": "golang",
  "hits": [
    { "id": 1, "title": "Learning Go" },
    { "id": 2, "title": "Go Concurrency Patterns" }
  ]
}

https://ithelp.ithome.com.tw/upload/images/20250919/20138331eq2tYk8fG7.png


小結

今天我們完成了:

  • 設計 /search API 規格
  • 用假資料先建立 API response 格式
  • 加上單元測試,驗證正常情況 & 缺少 query 的錯誤情況

這一步讓 API「看起來是真的」,雖然還沒接 Elasticsearch,但它已經是個完整的 endpoint,可以被前端或其他服務測試串接。


👉 明天我們要讓 API 更可靠:加入 context/timeout,避免下游請求無限卡住。



上一篇
Day 4 - 中介層:日誌、Recovery 與設定管理
下一篇
Day 6 - context/timeout:防止外呼卡死
系列文
用 Golang + Elasticsearch + Kubernetes 打造雲原生搜尋服務6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言